﻿/* EntitiesToDTOs. Copyright (c) 2011. Fabian Fernandez.
 * http://entitiestodtos.codeplex.com
 * Licensed by Common Development and Distribution License (CDDL).
 * http://entitiestodtos.codeplex.com/license
 * Fabian Fernandez. 
 * http://www.linkedin.com/in/fabianfernandezb/en
 * */
using EnvDTE;
using EnvDTE80;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.ComponentModel;
using EntitiesToDTOs.Properties;
using System.Threading;
using System.IO;
using EntitiesToDTOs.Events;
using EntitiesToDTOs.Helpers;
using EntitiesToDTOs.Enums;
using EntitiesToDTOs.Domain;
using EntitiesToDTOs.Generators.Parameters;

namespace EntitiesToDTOs.Generators
{
    /// <summary>
    /// Manages all operations related to the generation of DTOs.
    /// </summary>
    internal class DTOGenerator
    {
        /// <summary>
        /// Generates the DTOs of the EDMX Document provided using the parameters received.
        /// </summary>
        /// <param name="parameters">Parameters for the generation of DTOs.</param>
        /// <param name="worker">BackgroundWorker reference.</param>
        public static List<EntityDTO> GenerateDTOs(GenerateDTOsParams parameters, BackgroundWorker worker)
        {
            LogManager.LogMethodStart();

            if (parameters.DTOsServiceReady)
            {
                // Add Reference to System.Runtime.Serialization
                VisualStudioHelper.AddReferenceToProject(parameters.TargetProject,
                    Resources.AssemblySystemRuntimeSerialization, Resources.AssemblySystemRuntimeSerialization);
            }

            // Check Cancellation Pending
            if (GeneratorManager.CheckCancellationPending()) return null;

            // Variables
            EditPoint objEditPoint; // EditPoint to reuse
            CodeParameter objCodeParameter; // CodeParameter to reuse
            CodeNamespace objNamespace = null; // Namespace item to add Classes
            ProjectItem sourceFileItem = null; // Source File Item to save
            int dtosGenerated = 0;

            // Get the Entities DTOs to generate
            List<EntityDTO> entitiesDTOs = DTOGenerator.GetEntityDTOs(parameters.EDMXDocument,
                parameters.AssociationType, parameters.DTOsServiceReady);

            // Check Cancellation Pending
            if (GeneratorManager.CheckCancellationPending()) return null;

            // Report Progress
            worker.ReportProgress(0, new GeneratorOnProgressEventArgs(0, 
                string.Format(Resources.Text_DTOsGenerated, dtosGenerated, entitiesDTOs.Count)));

            // Create Template Class File
            TemplateClass.CreateFile();

            // Generate Source File if type is One Source File
            if (parameters.SourceFileGenerationType == SourceFileGenerationTypeEnum.OneSourceFile)
            {
                sourceFileItem = null;

                // Generate Source and Get the Namespace item
                objNamespace = VisualStudioHelper.GenerateSourceAndGetNamespace(parameters.TargetProject,
                    parameters.SourceFileName, parameters.SourceFileHeaderComment,
                    parameters.SourceNamespace, parameters.DTOsServiceReady, out sourceFileItem);
            }

            // Check Cancellation Pending
            if (GeneratorManager.CheckCancellationPending()) return null;

            // Loop through Entities DTOs
            foreach (EntityDTO entityDTO in entitiesDTOs)
            {
                // Generate Source File if type is Source File per Class
                if (parameters.SourceFileGenerationType == SourceFileGenerationTypeEnum.SourceFilePerClass)
                {
                    sourceFileItem = null;

                    // Generate Source and Get the Namespace item
                    objNamespace = VisualStudioHelper.GenerateSourceAndGetNamespace(
                        parameters.TargetProject, entityDTO.DTOName, parameters.SourceFileHeaderComment,
                        parameters.SourceNamespace, parameters.DTOsServiceReady, out sourceFileItem);
                }

                // Add Class
                CodeClass objCodeClass = objNamespace.AddClass(entityDTO.DTOName, AppConstants.PLACE_AT_THE_END,
                    entityDTO.DTOBaseName, null, entityDTO.ClassAccess);

                // Set the Class Kind
                (objCodeClass as CodeClass2).ClassKind = entityDTO.ClassKind;

                // Set IsAbstract
                objCodeClass.IsAbstract = entityDTO.IsAbstract;

                // Set Class Attributes
                foreach (EntityDTOAttribute classAttr in entityDTO.ClassAttributes)
                {
                    objCodeClass.AddAttribute(classAttr.Name, classAttr.Parameters, AppConstants.PLACE_AT_THE_END);
                }

                // Set Class Properties
                foreach (EntityDTOProperty entityProperty in entityDTO.Properties)
                {
                    // Add Property
                    CodeProperty objCodeProperty = objCodeClass.AddProperty(entityProperty.PropertyName,
                        entityProperty.PropertyName, entityProperty.PropertyType, AppConstants.PLACE_AT_THE_END,
                        entityProperty.PropertyAccess, null);

                    // Get end of accessors auto-generated code
                    objEditPoint = objCodeProperty.Setter.EndPoint.CreateEditPoint();
                    objEditPoint.LineDown();
                    objEditPoint.EndOfLine();
                    var getSetEndPoint = objEditPoint.CreateEditPoint();

                    // Move to the start of accessors auto-generated code
                    objEditPoint = objCodeProperty.Getter.StartPoint.CreateEditPoint();
                    objEditPoint.LineUp();
                    objEditPoint.LineUp();
                    objEditPoint.EndOfLine();

                    // Replace accessors auto-generated code with a more cleaner one
                    objEditPoint.ReplaceText(getSetEndPoint, Resources.CSharpCodeGetSetWithBrackets,
                        Convert.ToInt32(vsEPReplaceTextOptions.vsEPReplaceTextAutoformat));

                    // Set Property Attributes
                    foreach (EntityDTOAttribute propAttr in entityProperty.PropertyAttributes)
                    {
                        objCodeProperty.AddAttribute(propAttr.Name,
                            propAttr.Parameters, AppConstants.PLACE_AT_THE_END);
                    }

                    objEditPoint = objCodeProperty.StartPoint.CreateEditPoint();
                    objEditPoint.SmartFormat(objEditPoint);
                }

                if (parameters.GenerateDTOConstructors)
                {
                    // Add empty Constructor
                    CodeFunction emptyConstructor = objCodeClass.AddFunction(objCodeClass.Name,
                        vsCMFunction.vsCMFunctionConstructor, null, AppConstants.PLACE_AT_THE_END,
                        vsCMAccess.vsCMAccessPublic, null);

                    // Does this DTO have a Base Class ?
                    if (entityDTO.DTOBase != null)
                    {
                        // Add call to empty Base Constructor
                        objEditPoint = emptyConstructor.StartPoint.CreateEditPoint();
                        objEditPoint.EndOfLine();
                        objEditPoint.Insert(Resources.Space + Resources.CSharpCodeBaseConstructor);
                    }

                    // Does this DTO have properties ?
                    if (entityDTO.Properties.Count > 0)
                    {
                        // Add Constructor with all properties as parameters
                        CodeFunction constructorWithParams = objCodeClass.AddFunction(objCodeClass.Name,
                            vsCMFunction.vsCMFunctionConstructor, null, AppConstants.PLACE_AT_THE_END,
                            vsCMAccess.vsCMAccessPublic, null);

                        foreach (EntityDTOProperty entityProperty in entityDTO.Properties)
                        {
                            // Add Constructor parameter
                            objCodeParameter = constructorWithParams.AddParameter(
                                Utils.SetFirstLetterLowercase(entityProperty.PropertyName),
                                entityProperty.PropertyType, AppConstants.PLACE_AT_THE_END);

                            // Add assignment
                            objEditPoint = constructorWithParams.EndPoint.CreateEditPoint();
                            objEditPoint.LineUp();
                            objEditPoint.EndOfLine();
                            objEditPoint.Insert(Environment.NewLine + AppConstants.TAB + AppConstants.TAB + AppConstants.TAB);
                            objEditPoint.Insert(string.Format(Resources.CSharpCodeAssignmentThis,
                                entityProperty.PropertyName, objCodeParameter.Name));
                        }

                        // Does this DTO have a Base Class ?
                        if (entityDTO.DTOBase != null)
                        {
                            // Get the Base Class properties (includes the properties of the base recursively)
                            List<EntityDTOProperty> baseProperties =
                                DTOGenerator.GetPropertiesForConstructor(entityDTO.DTOBase);

                            // Base Constructor parameters
                            var sbBaseParameters = new StringBuilder();

                            foreach (EntityDTOProperty entityProperty in baseProperties)
                            {
                                // Add Constructor parameter
                                objCodeParameter = constructorWithParams.AddParameter(
                                    Utils.SetFirstLetterLowercase(entityProperty.PropertyName),
                                    entityProperty.PropertyType, AppConstants.PLACE_AT_THE_END);

                                // Add parameter separation if other parameters exists
                                if (sbBaseParameters.Length > 0)
                                {
                                    sbBaseParameters.Append(Resources.CommaSpace);
                                }

                                // Add to Base Constructor parameters
                                sbBaseParameters.Append(objCodeParameter.Name);
                            }

                            // Add call to Base Constructor with parameters
                            objEditPoint = constructorWithParams.StartPoint.CreateEditPoint();
                            objEditPoint.EndOfLine();
                            objEditPoint.Insert(
                                Environment.NewLine + AppConstants.TAB + AppConstants.TAB + AppConstants.TAB);
                            objEditPoint.Insert(
                                string.Format(Resources.CSharpCodeBaseConstructorWithParams, sbBaseParameters.ToString()));
                        
                        } // END if DTO has a Base Class

                    } // END if DTO has properties

                } // END if Generate DTO Constructor methods

                // Save changes to Source File Item
                sourceFileItem.Save();

                // Count DTO generated
                dtosGenerated++;

                // Report Progress
                int progress = ((dtosGenerated * 100) / entitiesDTOs.Count);
                if (progress < 100)
                {
                    worker.ReportProgress(progress, new GeneratorOnProgressEventArgs(progress, 
                        string.Format(Resources.Text_DTOsGenerated, dtosGenerated, entitiesDTOs.Count)));
                }

                // Check Cancellation Pending
                if (GeneratorManager.CheckCancellationPending()) return null;

            } // END Loop through Entities DTOs

            // Save Target Project
            parameters.TargetProject.Save();

            // Delete Template Class File
            TemplateClass.Delete();

            // Report Progress
            worker.ReportProgress(100, new GeneratorOnProgressEventArgs(100, 
                string.Format(Resources.Text_DTOsGenerated, dtosGenerated, entitiesDTOs.Count)));

            LogManager.LogMethodEnd();

            // Return the DTOs generated
            return entitiesDTOs;
        }

        /// <summary>
        /// Gets the objects that represents the DTOs that needs to be generated from the received EDMX Document.
        /// </summary>
        /// <param name="edmxDocument">EDMX Document containing the Entities definitions.</param>
        /// <param name="associationTypeDesired">Class Association Type desired.</param>
        /// <param name="makeDTOsServiceReady">Specifies if the DTOs are Service-Ready.</param>
        /// <returns></returns>
        private static List<EntityDTO> GetEntityDTOs(XDocument edmxDocument, 
            string associationTypeDesired, bool makeDTOsServiceReady)
        {
            LogManager.LogMethodStart();

            var complexTypeDTOs = new List<EntityDTO>();
            
            // Get ComplexType nodes
            IEnumerable<XElement> complexTypeNodeElements = edmxDocument.Descendants(EdmxNodes.ComplexType);

            // Get the DTOs for the ComplexType nodes
            foreach (XElement complexTypeNode in complexTypeNodeElements)
            {
                complexTypeDTOs.Add(new EntityDTO(complexTypeNode, null, makeDTOsServiceReady));
            }

            // Set the Complex Types available
            PropertyHelper.SetComplexTypes(complexTypeDTOs);

            // Get Navigation Properties
            List<EntityNavigation> entitiesNavigations = 
                DTOGenerator.GetEntitiesNavigations(edmxDocument, associationTypeDesired);

            // Get EntityType nodes
            IEnumerable<XElement> entityTypeNodeElements = edmxDocument.Descendants(EdmxNodes.EntityType);

            var entityTypeDTOs = new List<EntityDTO>();

            // Get the DTOs for the EntityType nodes
            foreach (XElement entityTypeNode in entityTypeNodeElements)
            {
                entityTypeDTOs.Add(new EntityDTO(entityTypeNode, entitiesNavigations, makeDTOsServiceReady));
            }

            foreach (EntityDTO dto in entityTypeDTOs)
            {
                // Set Know Types of DTO
                dto.SetKnownTypes(entityTypeDTOs, makeDTOsServiceReady);

                // Set DTOs childs of DTO
                dto.SetChilds(entityTypeDTOs);

                // Set reference to DTO Base Class
                dto.SetDTOBase(entityTypeDTOs);

                // Set Navigation Target DTO references
                foreach (EntityDTOProperty dtoProperty in dto.Properties.Where(p => p.IsNavigation == true))
                {
                    EntityDTO dtoTarget = entityTypeDTOs.FirstOrDefault(e => e.DTOName == dtoProperty.NavigatesToDTOName);

                    if (dtoTarget != null)
                    {
                        dtoProperty.SetNavigatesToDTOReference(dtoTarget);
                    }
                }
            }

            // Get the final result
            var result = new List<EntityDTO>();
            result.AddRange(complexTypeDTOs);
            result.AddRange(entityTypeDTOs);

            LogManager.LogMethodEnd();

            return result;
        }

        /// <summary>
        /// Gets the Associations of the provided EDMX Document.
        /// </summary>
        /// <param name="edmxDocument">EDMX Document containing the Entities definitions.</param>
        /// <returns></returns>
        private static List<EntityAssociation> GetEntitiesAssociations(XDocument edmxDocument)
        {
            LogManager.LogMethodStart();

            IEnumerable<XElement> associationNodeElements = edmxDocument.Descendants(EdmxNodes.Association);
            
            var result = new List<EntityAssociation>();

            foreach (XElement associationNodeElement in associationNodeElements)
            {
                result.Add(new EntityAssociation(associationNodeElement, EntityAssociationEndEnum.First));
                result.Add(new EntityAssociation(associationNodeElement, EntityAssociationEndEnum.Second));
            }

            LogManager.LogMethodEnd();

            return result;
        }

        /// <summary>
        /// Gets the Entity Keys of the provided EDMX Document.
        /// </summary>
        /// <param name="edmxDocument">EDMX Document containing the Entities definitions.</param>
        /// <returns></returns>
        private static List<EntityKeyProperty> GetEntitiesKeyProperties(XDocument edmxDocument)
        {
            LogManager.LogMethodStart();

            IEnumerable<XElement> entityNodeElements = 
                edmxDocument.Descendants(EdmxNodes.EntityType);
            
            var result = new List<EntityKeyProperty>();

            foreach (XElement entityNode in entityNodeElements)
            {
                result.AddRange(DTOGenerator.GetEntityKeys(entityNode, entityNode, entityNodeElements));
            }

            LogManager.LogMethodEnd();

            return result;
        }

        /// <summary>
        /// Gets the Keys of an Entity.
        /// </summary>
        /// <param name="entityNodeKeysOwner">EntityType node to get the keys from.</param>
        /// <param name="entityNodeToSetKeys">EntityType node to set the keys.</param>
        /// <param name="entityNodeElements">EntityType nodes.</param>
        /// <returns></returns>
        private static List<EntityKeyProperty> GetEntityKeys(XElement entityNodeKeysOwner, XElement entityNodeToSetKeys, 
            IEnumerable<XElement> entityNodeElements)
        {
            LogManager.LogMethodStart();

            var result = new List<EntityKeyProperty>();

            IEnumerable<XElement> propertyRefNodeElements = entityNodeKeysOwner.Descendants(EdmxNodes.PropertyRef);

            IEnumerable<XElement> propertyNodeElements = entityNodeKeysOwner.Descendants(EdmxNodes.Property);

            foreach (var propertyRefNode in propertyRefNodeElements)
            {
                result.Add(new EntityKeyProperty(entityNodeToSetKeys, propertyRefNode, propertyNodeElements));
            }

            // Does this Entity have a Base Type ?
            string entityBaseType = EntityDTOHelper.GetEntityBaseType(entityNodeKeysOwner);
            if (entityBaseType != null)
            {
                // Find the Base Type node
                XElement entityBaseTypeNode = entityNodeElements.FirstOrDefault(
                    e => e.Attribute(EdmxNodeAttributes.EntityType_Name).Value == entityBaseType);

                if (entityBaseTypeNode == null)
                {
                    throw new Exception(string.Format(Resources.Error_BaseTypeNotFound,
                        entityNodeKeysOwner.Attribute(EdmxNodeAttributes.EntityType_Name).Value, entityBaseType));
                }

                // Add the Entity Base Type keys to the resulting keys
                result.AddRange(DTOGenerator.GetEntityKeys(entityBaseTypeNode, entityNodeToSetKeys, entityNodeElements));
            }

            LogManager.LogMethodEnd();

            return result;
        }

        /// <summary>
        /// Gets the Entity Navigations from the provided EDMX Document.
        /// </summary>
        /// <param name="edmxDocument">EDMX Document containing the Entities definitions.</param>
        /// <param name="associationTypeDesired">Class Association Type desired.</param>
        /// <returns></returns>
        private static List<EntityNavigation> GetEntitiesNavigations(XDocument edmxDocument, 
            string associationTypeDesired)
        {
            LogManager.LogMethodStart();

            // Get Entity Associations
            List<EntityAssociation> entitiesAssociations = DTOGenerator.GetEntitiesAssociations(edmxDocument);

            // Get Entity Key Properties
            List<EntityKeyProperty> entitiesKeys = DTOGenerator.GetEntitiesKeyProperties(edmxDocument);

            // Get Entities NavigationProperty Nodes
            IEnumerable<XElement> navigationNodeElements = edmxDocument.Descendants(EdmxNodes.NavigationProperty);
            
            var result = new List<EntityNavigation>();

            foreach (XElement navigationNode in navigationNodeElements)
            {
                result.Add(new EntityNavigation(navigationNode, entitiesAssociations, 
                    associationTypeDesired, entitiesKeys));
            }

            LogManager.LogMethodEnd();

            return result;
        }

        /// <summary>
        /// Gets the Properties of an EntityDTO to use in a Constructor as parameters, recursively including the Base Class properties.
        /// </summary>
        /// <param name="entityDTO">EntityDTO to get the Properties from.</param>
        /// <returns></returns>
        private static List<EntityDTOProperty> GetPropertiesForConstructor(EntityDTO entityDTO)
        {
            LogManager.LogMethodStart();

            var result = new List<EntityDTOProperty>();

            result.AddRange(entityDTO.Properties);

            if (entityDTO.DTOBase != null)
            {
                result.AddRange(DTOGenerator.GetPropertiesForConstructor(entityDTO.DTOBase));
            }

            LogManager.LogMethodEnd();

            return result;
        }
    }
}